//
//  PhotoBrowserCollectionViewController.swift
//  Photomania
//
//  Created by Essan Parto on 2014-08-20.
//  Copyright (c) 2014 Essan Parto. All rights reserved.
//

import UIKit
import Alamofire

class PhotoBrowserCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
  var photos = NSMutableOrderedSet()
  let imageCache = NSCache() //创建了一个用于缓存图片的NSCache对象
  let refreshControl = UIRefreshControl()
  
  //这里我们定义了两个变量，来记录当前是否在更新照片，以及当前我们正在浏览的是哪一个照片页面。
  var populatingPhotos = false
  var currentPage = 1
  
  let PhotoBrowserCellIdentifier = "PhotoBrowserCell"
  let PhotoBrowserFooterViewIdentifier = "PhotoBrowserFooterView"
  
  // MARK: Life-cycle
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    setupView()
    
    populatePhotos()
  }
  
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
  
  // MARK: CollectionView
  
  override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return photos.count
  }
  
  
  /*
  Alamofire 的网络调用是异步请求方式。这意味着提交网络请求不会阻止剩余代码的执行。网络请求可能会执行很长时间才能得到返回结果，但是您不会希望在等待图片下载的时候 UI 被冻结。
  
  也就是说，实现异步请求是一个极大的挑战。如果在发出请求之后到从服务器接收到响应的这段时间中，UI 发生了改变的话怎么办？
  
  例如，UICollectionView拥有内部的单元出列机制。创建新的单元对系统来说开销很大，因此集合视图将重用不在屏幕上显示的现有单元，而不是不停创建新的单元。
  
  这意味着同一个单元对象，会被不停地重复使用。因此在发出 Alamofire 请求之后到接收到图片信息响应之前，用户将单元滚动出屏幕并且删除图片的操作将成为可能。单元可能会出列，并且准备显示另一个图片。
  
  在collectionView(_: cellForItemAtIndexPath:)的代码中，我们完成了两件事来解决这个问题。第一件事是，当一个单元出列后，我们通过设值为nil的方法来清除图片。这个操作确保我们不会显示原先的图片；第二件事是，我们的请求完成处理方法将检查单元的 URL 是否和请求的 URL 相等。如果不相等的话，显然单元已经拥有了另外的图片，那么完成处理方法将不会浪费其生命周期来为单元设置错误的图片。
  */
  /*
  
  1.出队的单元可能已经有一个连带的 Alamorire 请求。检查这个请求是否相关，也就是说，检查该请求的 URL 是否和要显示的图片 URL 相匹配，否则就取消请求。
  2.使用可选值绑定来检查该图片是否有缓存版本。如果有的话，使用该缓存版本而不是再次下载。
  3.如果没有相应的缓存版本的话，那么就下载它。然后，出列单元可能已经显示出了另一幅图像。这样子的话，就将其设置为nil，因此当图片在下载时该单元将为空。
  4.从服务器下载图片，然后验证返回响应的content-type。如果返回的不是图片，那么返回值就是为error。因此您就不会再对这个无效的图片进行操作了。这里的关键是我们在单元中存储了 Alamofire 请求对象，当网络异步调用返回时使用。
  5.如果我们没有接收到错误信息，那么就下载相应的图片，并在随后缓存它。
  6.检查单元是否出列以显示新的图片。如果没有的话，则相应地设置单元的图片。

  */
  override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(PhotoBrowserCellIdentifier, forIndexPath: indexPath) as! PhotoBrowserCollectionViewCell
    
    let imageURL = (photos.objectAtIndex(indexPath.row) as! PhotoInfo).url
    
    // 1
    cell.request?.cancel()
    
    // 2
    if let image = self.imageCache.objectForKey(imageURL) as? UIImage {
      cell.imageView.image = image
    } else {
      // 3
      cell.imageView.image = nil
      
      // 4
      cell.request = Alamofire.request(.GET, imageURL).validate(contentType: ["image/*"]).responseImage() {
        (request, _, image, error) in
        if error == nil && image != nil {
          // 5
          self.imageCache.setObject(image!, forKey: request.URLString)
          
          // 6
          cell.imageView.image = image
        } else {
          /*
          如果单元格在图片下载完成之前离开了屏幕，那么我们将暂停下载，并且返回一个NSURLErrorDomain (-999: cancelled)对象。这是业界普遍的处理方式。
          */
        }
      }
    }
    
    return cell
  }
  
  override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
    return collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: PhotoBrowserFooterViewIdentifier, forIndexPath: indexPath) as! UICollectionReusableView
  }
  
  override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
    performSegueWithIdentifier("ShowPhoto", sender: (self.photos.objectAtIndex(indexPath.item) as! PhotoInfo).id)
  }
  
  // MARK: Helper
  
  func setupView() {
    navigationController?.setNavigationBarHidden(false, animated: true)
    
    let layout = UICollectionViewFlowLayout()
    let itemWidth = (view.bounds.size.width - 2) / 3
    layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
    layout.minimumInteritemSpacing = 1.0
    layout.minimumLineSpacing = 1.0
    layout.footerReferenceSize = CGSize(width: collectionView!.bounds.size.width, height: 100.0)
    
    collectionView!.collectionViewLayout = layout
    
    navigationItem.title = "Featured"
    
    collectionView!.registerClass(PhotoBrowserCollectionViewCell.classForCoder(), forCellWithReuseIdentifier: PhotoBrowserCellIdentifier)
    collectionView!.registerClass(PhotoBrowserCollectionViewLoadingCell.classForCoder(), forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: PhotoBrowserFooterViewIdentifier)
    
    refreshControl.tintColor = UIColor.whiteColor()
    refreshControl.addTarget(self, action: "handleRefresh", forControlEvents: .ValueChanged)
    collectionView!.addSubview(refreshControl)
  }
  
  override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "ShowPhoto" {
      (segue.destinationViewController as! PhotoViewerViewController).photoID = sender!.integerValue
      (segue.destinationViewController as! PhotoViewerViewController).hidesBottomBarWhenPushed = true
    }
  }
  
  /*
  
  1.一旦您滚动超过了 80% 的页面，那么scrollViewDidScroll()方法将会加载更多的图片。
  
  2.populatePhotos()方法在currentPage当中加载图片，并且使用populatingPhotos作为标记，以防止还在加载当前界面时加载下一个页面。
  
  3.这里我们首次使用了我们创建的路由。只需将页数传递进去，它将为该页面构造 URL 字符串。500px.com 网站在每次 API 调用后返回大约50张图片，因此您需要为下一批照片的显示再次调用路由。
  
  4.要注意，.responseJSON()后面的代码块：completion handler(完成处理方法)必须在主线程运行。如果您正在执行其他的长期运行操作，比如说调用 API，那么您必须使用 GCD 来将您的代码调度到另一个队列运行。在本示例中，我们使用DISPATCH_QUEUE_PRIORITY_HIGH来运行这个操作。
  
  5.您可能会关心 JSON 数据中的photos关键字，其位于数组中的字典中。每个字典都包含有一张图片的信息。
  
  6.我们使用 Swift 的filter函数来过滤掉 NSFW 图片（Not Safe For Work）
  
  7.map函数接收了一个闭包，然后返回一个PhotoInfo对象的数组。这个类是在Five100px.swift当中定义的。如果您查看这个类的源码，那么就可以看到它重写了isEqual和hash这两个方法。这两个方法都是用一个整型的 id 属性，因此排序和唯一化（uniquing）PhotoInfo对象仍会是一个比较快的操作
  
  8.接下来我们会在添加新的数据前存储图片的当前数量，使用它来更新collectionView
  
  9.如果有人在我们滚动前向 500px.com 网站上传了新的图片，那么您所获得的新的一批照片将可能会包含一部分已下载的图片。这就是为什么我们定义var photos = NSMutableOrderedSet()为一个组。由于组内的项目必须唯一，因此重复的图片不会再次出现
  
  10.这里我们创建了一个NSIndexPath对象的数组，并将其插入到collectionView
  
  11.在集合视图中插入项目，请在主队列中完成该操作，因为所有的 UIKit 操作都必须运行在主队列中

  */
  
  // 1
  override func scrollViewDidScroll(scrollView: UIScrollView) {
    if scrollView.contentOffset.y + view.frame.size.height > scrollView.contentSize.height * 0.8 {
      populatePhotos()
    }
  }
  
  func populatePhotos() {
    // 2
    if populatingPhotos {
      return
    }
    
    populatingPhotos = true
    
    // 3
    Alamofire.request(Five100px.Router.PopularPhotos(self.currentPage)).responseJSON() {
      (_, _, JSON, error) in
      
      if error == nil {
        // 4
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
          // 5, 6, 7
          let photoInfos = ((JSON as! NSDictionary).valueForKey("photos") as! [NSDictionary]).filter({ ($0["nsfw"] as! Bool) == false }).map { PhotoInfo(id: $0["id"] as! Int, url: $0["image_url"] as! String) }
          
          // 8
          let lastItem = self.photos.count
          // 9
          self.photos.addObjectsFromArray(photoInfos)
          
          // 10
          let indexPaths = (lastItem..<self.photos.count).map { NSIndexPath(forItem: $0, inSection: 0) }
          
          // 11
          dispatch_async(dispatch_get_main_queue()) {
            self.collectionView!.insertItemsAtIndexPaths(indexPaths)
          }
          
          self.currentPage++
        }
      }
      self.populatingPhotos = false
    }
  }
  
  /*
  清空您当前的模式（self.photos)，然后重置currentPage，最后刷新 UI。
  */
  func handleRefresh() {
    refreshControl.beginRefreshing()
    
    self.photos.removeAllObjects()
    self.currentPage = 1
    
    self.collectionView!.reloadData()
    
    refreshControl.endRefreshing()
    
    populatePhotos()
  }
}

class PhotoBrowserCollectionViewCell: UICollectionViewCell {
  let imageView = UIImageView()
  
  var request: Alamofire.Request? //这个属性会为这个单元存储 Alamofire 的请求来加载图片
  
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    
    backgroundColor = UIColor(white: 0.1, alpha: 1.0)
    
    imageView.frame = bounds
    addSubview(imageView)
  }
}

class PhotoBrowserCollectionViewLoadingCell: UICollectionReusableView {
  let spinner = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.WhiteLarge)
  
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    
    spinner.startAnimating()
    spinner.center = self.center
    addSubview(spinner)
  }
}
